-module(ase_parser).
-export([parse/1]).

parse(FileName) ->
    {ok, File} = file:open(FileName, read),
    parse(get_token(File), File, scene:new()).
  
%% Main objects
parse_next(File, Scene) -> parse(get_token(File), File, Scene).

parse("LIGHTOBJECT", File, Scene) ->
    check_tokens_exist(File, ["{"]),
    parse_next(File, parse_light(File, Scene));
parse("CAMERAOBJECT", File, Scene) ->
    check_tokens_exist(File, ["{"]),
    parse_next(File, parse_camera(File, Scene));
parse("GEOMOBJECT", File, Scene) ->
    check_tokens_exist(File, ["{"]),
    parse_next(File, parse_geom(File, Scene));
parse("MATERIAL_LIST", File, Scene) ->
    check_tokens_exist(File, ["{", "MATERIAL_COUNT"]),
    NewScene = case parse_int(get_token(File)) of
        0 -> Scene;
        _ -> parse_materials(File, Scene)
    end,
    parse_next(File, NewScene);
parse("SCENE", File, Scene) ->
    check_tokens_exist(File, ["{"]),
    parse_next(File, parse_scene(File, Scene));
parse("COMMENT", File, Scene) ->
    Comment = get_token(File),
    error_logger:info_msg("Parsing file: ~p~n", [Comment]),
    parse_next(File, Scene);
parse("3DSMAX_ASCIIEXPORT", File, Scene) -> 
    skip_tokens(File, 1),   %% Ignore the version number
    parse_next(File, Scene);
parse(eof, _File, Scene) ->  Scene;
parse(_Ignored, File, Scene) ->
    parse_next(File, Scene).

% Lights
parse_light(File, Scene) ->
    TM = get_tm(File),
    Position = vector:transform(TM, vector:o()),
    skip_to_token(File, "LIGHT_COLOR"),
    Color = get_color(File),
    skip_to_token(File, "LIGHT_INTENS"),
    Intensity = parse_float(get_token(File)),
    scene:add_light(light:new(Position, Color, Intensity), Scene).

% Camera
parse_camera(File, Scene) ->
    PositionTM = get_tm(File),
    TargetTM = get_tm(File),

    Position = vector:transform(PositionTM, vector:new(1, 1, 1)),
    Target = vector:transform(lists:sublist(TargetTM, 9), vector:o()),
    Up = vector:zn(),
    
    skip_to_token(File, "CAMERA_FOV"),
    FOV = parse_float(get_token(File))*60,
    scene:set_camera(camera:new(FOV, Position, Target, Up), Scene).

%Objects
-record(geom, {vertices=[], faces=[], normals=[], mtlid=0}).

parse_geom(File, Scene) ->
    %TM = get_tm(File),
    
    skip_to_token(File, "MESH"),
    check_tokens_exist(File, ["{"]),
    
    MeshObject = parse_mesh(File, #geom{}),
    Obj = get_mtl_ref(File, MeshObject),

    Vertices = Obj#geom.vertices,
    Normals =  Obj#geom.normals,

    Faces = build_faces(Obj#geom{vertices=Vertices, normals=Normals}, []),
    Box = bbox:build([ V || {_ID, V} <- Vertices]),

    scene:add_object(object:new(Faces, Box), Scene).



build_faces(#geom{faces=[]}, Faces) -> Faces;
build_faces(Obj=#geom{faces=[Face|Tail]}, Faces) ->
    {FaceId, IdA, IdB, IdC} = Face,
    NewFace = face:new(
        Obj#geom.mtlid,
        get_by_id(IdA, Obj#geom.vertices),
        get_by_id(IdB, Obj#geom.vertices),
        get_by_id(IdC, Obj#geom.vertices),
        get_by_id(FaceId, Obj#geom.normals)
    ),
    build_faces(Obj#geom{faces=Tail}, [NewFace | Faces]).

get_by_id(Id, List) -> element(2, lists:keyfind(Id, 1, List)).

parse_mesh(File, Obj=#geom{}) ->
    parse_mesh_props(get_token(File), File, Obj).

get_mtl_ref(File, Obj=#geom{}) ->
    skip_to_token(File, "MATERIAL_REF"),
    Obj#geom{mtlid=parse_int(get_token(File))}.

parse_mesh_props("MESH_NUMTVFACES", File, Obj=#geom{}) ->
    Num = parse_int(get_token(File)),
    skip_tokens(File, Num*5+3),
    parse_mesh(File, Obj);
parse_mesh_props("MESH_NUMTVERTEX", File, Obj=#geom{}) ->
    Num = parse_int(get_token(File)),
    skip_tokens(File, Num*5+3),
    parse_mesh(File, Obj);
parse_mesh_props("MESH_NORMALS", File, Obj=#geom{}) ->
    check_tokens_exist(File, ["{"]),
    parse_mesh(File, Obj#geom{normals=get_mesh_normals(File, [])});
parse_mesh_props("MESH_FACE_LIST", File, Obj=#geom{}) ->
    check_tokens_exist(File, ["{"]),
    parse_mesh(File, Obj#geom{faces=get_mesh_faces(File, [])});
parse_mesh_props("MESH_VERTEX_LIST", File, Obj=#geom{}) ->
    check_tokens_exist(File, ["{"]),
    parse_mesh(File, Obj#geom{vertices=get_mesh_vertices(File, [])});
parse_mesh_props("}", _File, Obj=#geom{}) -> Obj;
parse_mesh_props(_Ignore, File, Obj=#geom{}) ->
    parse_mesh(File, Obj).

get_mesh_normals(File, Normals) ->
    case get_mesh_normal(File) of
        false -> Normals;
        Normal -> get_mesh_normals(File, [Normal | Normals])
    end.
get_mesh_normal(File) ->
    case get_token(File) of
        "MESH_FACENORMAL" ->
            FaceId = parse_int(get_token(File)),
            X = parse_float(get_token(File)),
            Y = parse_float(get_token(File)),
            Z = parse_float(get_token(File)),
            skip_tokens(File, 15),
            {FaceId, vector:normalize(vector:new(X, Y, Z))};
        _ -> false
    end.

get_mesh_faces(File, Faces) ->
    case get_mesh_face(File) of
        false -> Faces;
        Face -> get_mesh_faces(File, [Face | Faces])
    end.
get_mesh_face(File) ->
    case get_token(File) of
        "MESH_FACE" ->
            Id = parse_int(get_token(File)),
            skip_tokens(File, 1),
            A = parse_int(get_token(File)),
            skip_tokens(File, 1),
            B = parse_int(get_token(File)),
            skip_tokens(File, 1),
            C = parse_int(get_token(File)),
            
            skip_to_token(File, "MESH_MTLID"),
            skip_tokens(File, 1),

            {Id, A, B, C};
        _ -> false
    end.


get_mesh_vertices(File, Vertices) ->
    case get_mesh_vertex(File) of
        false -> Vertices;
        Vertex -> get_mesh_vertices(File, [Vertex | Vertices])
    end.
get_mesh_vertex(File) ->
    case get_token(File) of
        "MESH_VERTEX" ->
            Id = parse_int(get_token(File)),
            X = parse_float(get_token(File)),
            Y = parse_float(get_token(File)),
            Z = parse_float(get_token(File)),
            {Id, vector:new(X, Y, Z)};
        _ -> false
    end.

%% Materials
parse_materials(File, Scene) ->
    parse_material(get_token(File), File, Scene).
    
parse_material("MATERIAL", File, Scene) ->
    ID = parse_int(get_token(File)),
    check_tokens_exist(File, ["{"]),
    skip_to_token(File, "MATERIAL_DIFFUSE"),
    Color = get_color(File),
    skip_to_token(File, "MATERIAL_SHINE"),
    S = parse_float(get_token(File)),
    skip_to_token(File, "MATERIAL_SHINESTRENGTH"),
    SS = parse_float(get_token(File)),
    Roughness = 1/(S*SS),

    skip_to_token(File, "MATERIAL_TRANSPARENCY"),
    Transmittance = parse_float(get_token(File)),

    Material = material:new(ID, Roughness, Transmittance, 1.55, Color),

    skip_to_token(File, "}"),
    parse_materials(File, scene:add_material(Material, Scene));
parse_material("}", _File, Scene) -> Scene.

%% Scene
parse_scene(File, Scene) ->
    parse_scene_prop(get_token(File), File, Scene).

parse_scene_prop("SCENE_AMBIENT_STATIC", File, Scene) ->
    parse_scene(File, scene:set_ambient(get_color(File), Scene));
parse_scene_prop("SCENE_BACKGROUND_STATIC", File, Scene) ->
    parse_scene(File, scene:set_background(get_color(File), Scene));
parse_scene_prop("}", _File, Scene) -> Scene;
parse_scene_prop(_Ignored, File, Scene) ->
    skip_tokens(File, 1),
    parse_scene(File, Scene).

%% Misc functions
get_tm(File) ->
    skip_to_token(File, "NODE_TM"),
    check_tokens_exist(File, ["{"]),
    
    skip_to_token(File, "TM_ROW0"),
    T1 = parse_float(get_token(File)),
    T2 = parse_float(get_token(File)),
    T3 = parse_float(get_token(File)),

    check_tokens_exist(File, ["TM_ROW1"]),
    T4 = parse_float(get_token(File)),
    T5 = parse_float(get_token(File)),
    T6 = parse_float(get_token(File)),

    check_tokens_exist(File, ["TM_ROW2"]),
    T7 = parse_float(get_token(File)),
    T8 = parse_float(get_token(File)),
    T9 = parse_float(get_token(File)),

    check_tokens_exist(File, ["TM_ROW3"]),
    Ox = parse_float(get_token(File)),
    Oy = parse_float(get_token(File)),
    Oz = parse_float(get_token(File)),

    skip_to_token(File, "}"),

    [T1, T2, T3, T4, T5, T6, T7, T8, T9, Ox, Oy, Oz].

skip_to_token(File, Token) ->
    case get_token(File) of
        Token -> ok;
        _ -> skip_to_token(File, Token)
    end.

check_tokens_exist(_File, []) -> ok;
check_tokens_exist(File, [Token|Tail]) ->
    case get_token(File) of
        Token -> check_tokens_exist(File, Tail);
        _Other ->
            error_logger:error_msg("Expecting token ~p, got ~p instead.~n", [Token, _Other]),
            exit("Invalid sintax")
    end.

skip_tokens(_File, 0) -> ok;
skip_tokens(File, Num) ->
    get_token(File),
    skip_tokens(File, Num-1).

parse_float(String) ->
    case string:to_float(String) of
       {error, _Msg} -> 0.0;
       {Val, _Rest}  -> Val
    end.

parse_int(String) ->
    {Val, _} = string:to_integer(String),
    Val.

get_color(File) ->
    R = parse_float(get_token(File)),
    G = parse_float(get_token(File)),
    B = parse_float(get_token(File)),
    color:new(R, G, B).

%% Lexer
get_token(File) -> get_token(File, [], false).

get_token(File, Token, false) ->
    Char = get_char(File),
    case Char of
        eof -> eof;
        "\"" -> get_token(File, Token, true);
        "*" -> get_token(File, Token, false);
        ":" -> get_token(File, Token, false);
        "\r" -> check_token(File, Token);
        "\n" -> check_token(File, Token);
        " " -> check_token(File, Token);
        "\t" -> check_token(File, Token);
        Other -> get_token(File, Token++Other, false)
    end;
get_token(File, Token, true) ->
    Char = get_char(File),
    case Char of
        eof -> eof;
        "\"" ->
            case lists:last(Token) of
                "\\" -> get_token(File, Token+"\"", true);
                _Other -> get_token(File, Token, false)
            end;
        Other -> get_token(File, Token++Other, true)
    end.

get_char(File) ->
    case file:read(File, 1) of
        {ok, Char} -> Char;
        eof -> eof
    end.

check_token(File, []) -> get_token(File, [], false);
check_token(_File, Token) -> string:to_upper(Token).